/*
 *  Copyright 2007 Marc Lanctot and Marc Gendron-Bellemare
 *
 *  This file is part of OpenMTG.
 *
 *  OpenMTG is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  OpenMTG is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with OpenMTG.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

import java.net.*;
import java.io.*;

public class Client {

  private String playerName;
  private String hostname;
  private String deckName;
  private int serverPort;
  private UI userInterface;

  private Socket s;
  private PrintWriter out;
  private BufferedReader in;

  protected Player me;
  protected GameState state;

  protected int phase;
  // 1 = deciding who starts (me)
  // 2 = deciding who starts (opponent)
  // 3 = mulligans, I am starting
  // 4 = mulligans, opponent is starting
  // 5 = started

  protected int mulligans;

  protected int turnPlayerId;
  protected int turn;

  protected boolean doPrintMessages;

  public Client(String playerName, String hostname, int serverPort,
      String deckName) {
    this.playerName = playerName;
    this.hostname = hostname;
    this.serverPort = serverPort;
    this.deckName = deckName;
    this.state = null;

    phase = 0;
    mulligans = 0;
    turn = 0;
    doPrintMessages = false; 
  }

  public static void usage() {
    System.out.println("java Client <options...>");
    System.out.println("");
    System.out.println("Valid options:");
    System.out
        .println("   --deck <name>            (required. eg 'test' is decks/test.deck)");
    System.out.println("   --host <name>            (required)");
    System.out.println("   --name <name>            (required)");
    System.out.println("   --port <num>             (required)");
    System.out
        .println("   --nocolor                (optional, removes color from text interface)");
    System.out.println("");
    System.exit(-1);
  }

  public static void main(String[] args) throws IOException {

    String host = null;
    String name = null;
    String deck = null;
    int port = -1;
    boolean verbose = false;

    for (int i = 0; i < args.length; i++) {
      if (args[i].equals("--port")) {
        if (++i >= args.length)
          usage();
        port = Integer.parseInt(args[i]);
      } else if (args[i].equals("--host")) {
        if (++i >= args.length)
          usage();
        host = args[i];
      } else if (args[i].equals("--name")) {
        if (++i >= args.length)
          usage();
        name = args[i];
      } else if (args[i].equals("--deck")) {
        if (++i >= args.length)
          usage();
        deck = args[i];
      } else if (args[i].equals("--nocolor")) {
        UI.color = false;
      } else if (args[i].equals("--verbose") || args[i].equals("-v")) {
        verbose = true;
      }
    }

    if (port < 0 || host == null || name == null || deck == null)
      usage();

    Client c = new Client(name, host, port, deck);
    c.setPrintMessages(verbose);
    c.run();
  }

  public void setPrintMessages(boolean p) {
    doPrintMessages = p;
  }

  public boolean uiEnabled()
  {
    return true; 
  }
  
  public boolean getPrintMessages() {
    return doPrintMessages;
  }

  private void cleanup() {
    try {
      out.close();
      in.close();
      // stdIn.close();
      s.close();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void send(String s) {
    out.println(s);
  }
  
  public void uiShowMsg(String s) {
    if (userInterface != null)
      userInterface.showMsg(s); 
  }
  
  public void connect()
  {
    try {
      s = new Socket(hostname, serverPort);
      out = new PrintWriter(s.getOutputStream(), true);
      in = new BufferedReader(new InputStreamReader(s.getInputStream()));
    } catch (UnknownHostException e) {
      System.err.println("Don't know about host: " + hostname);
      System.exit(1);
    } catch (IOException e) {
      System.err.println("Couldn't get I/O for " + "the connection to: "
          + hostname);
      System.exit(1);
    }    
  }

  public void run() {
    
    connect(); 

    String inputLine;

    if (uiEnabled())
      userInterface = new SimpleTextUI(this);
    
    state = new GameState();

    // Send player info (now, just the name. Later, more)
    out.println("PLAYERINFO " + playerName);

    // Send deck info (now, just the name.. deck loaded on the server. Later,
    // more)
    out.println("DECKINFO " + deckName);

    try {

      inputLine = in.readLine();

      while (!inputLine.equals("exit")) {
        if (inputLine == null)
          break;

        if (doPrintMessages)
          System.out.println("Received: " + inputLine);

        // process it?

        handleEvent(inputLine);

        // else:
        if (userInterface != null)
          userInterface.handleEvent(inputLine);

        inputLine = in.readLine();
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

    cleanup();
  }

  // (network) event handlers

  private void handleEvent(String inputLine) {
    String[] args = inputLine.split(" ");
    if (args.length < 1)
      return;
    String cmd = args[0];

    if (cmd.equals("PLAYERINFO"))
      handlePlayerInfo(inputLine);
    else if (cmd.equals("DRAW"))
      handleDraw(inputLine);
    else if (cmd.equals("MULLIGAN"))
      handleMulligan(inputLine);
    else if (cmd.equals("STARTTURN"))
      handleStartTurn(inputLine);
    else if (cmd.equals("PLAYLAND"))
      handlePlayLand(inputLine);
    else if (cmd.equals("TAPFORMANA"))
      handleTapForMana(inputLine);
    else if (cmd.equals("CAST"))
      handleCast(inputLine);
    else if (cmd.equals("ATKRESULTS"))
      handleAttackResults(inputLine);
    else if (cmd.equals("ATTACK"))
      handleAttack(inputLine);
    else if (cmd.equals("KILL"))
      handleKill(inputLine);
    else if (cmd.equals("PLAYERDRAW"))
      handlePlayerDraw(inputLine);
    else if (cmd.equals("RESEFF"))
      handleResEff(inputLine);
    else if (cmd.equals("UNTAP"))
      handleUntap(inputLine);
    else if (cmd.equals("UNSICK"))
      handleUnsick(inputLine);
    else if (cmd.equals("DISCARD"))
      handleDiscard(inputLine);
    else if (cmd.equals("DAMAGE"))
      handleDamage(inputLine);
    else if (cmd.equals("REMOVE"))
      handleRemove(inputLine);
    else if (cmd.equals("GRAVEYARD"))
      handleGraveyard(inputLine);
    else if (cmd.equals("PHASE"))
      handlePhase(inputLine);
    
    post_handle_hook(inputLine); 
  }

  // To be overridden
  protected void post_handle_hook(String inputLine)
  {
  }
  
  private void handleKill(String inputLine) {
    System.out.println("Client killed by server. ");
    System.exit(-1);
  }

  private void handlePlayerInfo(String inputLine) {
    String[] parts = inputLine.split(" ");
    int id = Integer.parseInt(parts[1]);
    String name = parts[2];
    int lifePoints = Integer.parseInt(parts[3]);
    int numCards = Integer.parseInt(parts[4]);
    int libsize = Integer.parseInt(parts[5]);

    Player p = state.getPlayer(id);
    boolean newplayer = false;

    if (p == null) {
      newplayer = true;
      p = new Player(id, name);
    }

    p.setLifePoints(lifePoints);
    p.setHandSize(numCards);
    p.libsize = libsize;

    // bad should change this, or check on server-side

    if (newplayer) {
      if (name.equals(playerName)) {
        me = p;

        if (userInterface != null)
        {
          userInterface.setState(state);
          userInterface.setPlayer(me);
          userInterface.start();
        }
      }

      state.addPlayer(p);
    }
  }

  private void handleDraw(String inputLine) {
    String parts[] = inputLine.split(" ");

    String cardParts[] = parts[1].split(",");

    int cid = Integer.parseInt(cardParts[0]);
    int iid = Integer.parseInt(cardParts[1]);

    int libsize = Integer.parseInt(parts[2]);

    CardLoader cl = new CardLoader();
    Card c = cl.getCard(cid);
    c.setIID(iid);

    me.libsize = libsize;

    me.getHand().add(c);
  }

  private void handleMulligan(String inputLine) {
    String[] parts = inputLine.split(" ");
    int code = Integer.parseInt(parts[1]);

    if (code == 1)
      me.clearHand();
  }

  private void handleStartTurn(String inputLine) {
    String[] parts = inputLine.split(" ");

    phase = 5;

    turnPlayerId = Integer.parseInt(parts[1]);
    turn = Integer.parseInt(parts[2]);

    // Automatically send a 'GO' to let the server run through the beginning
    // phases
    if (turnPlayerId == me.getId())
      send("STARTTURN");
    // showState();
  }

  private void handlePlayLand(String inputLine) {
    String[] parts = inputLine.split(" ");

    int pid = Integer.parseInt(parts[1]);

    CardLoader cl = new CardLoader();
    Card c = cl.decode(parts[2]);

    Player p = state.getPlayer(pid);

    // remove if from our hand if it's us. otherwise, it's opponent
    Card card = p.getCardInHand(c.getIID());
    if (card != null) {
      p.removeCardFromHand(card);
    } else
      p.handSize--;

    p.lands.add(c);
    c.setSickness(true);
  }

  private void handleTapForMana(String inputLine) {
    String[] parts = inputLine.split(" ");
    // @@@ won't this crash if parts.length == 1?
    int pid = Integer.parseInt(parts[1]);

    for (int i = 2; i < parts.length; i++) {
      int iid = Integer.parseInt(parts[i]);

      if (!state.tapForMana(pid, iid))
        uiShowMsg("Tap for mana failed for pid " + pid + ", iid " + iid);
    }
  }

  private void handleCast(String inputLine) {
    String[] parts = inputLine.split(" ");
    int pid = Integer.parseInt(parts[1]);

    String manaPool = (parts.length <= 3 ? "" : parts[3]);

    CardLoader cl = new CardLoader();
    Card c = cl.decode(parts[2]);

    Player p = state.getPlayer(pid);

    // remove if from our hand if it's us. otherwise, it's opponent
    Card card = p.getCardInHand(c.getIID());
    if (card != null) {
      p.removeCardFromHand(card);
      p.manaPool = manaPool;
      c = card;

      
      uiShowMsg("You are casting " + card.toString());
      uiShowMsg("Respond with a spell/effect or press 'd' to resolve the stack.");
      
    } else {
      p.handSize--;
      p.manaPool = manaPool;

      
      uiShowMsg(p.getName() + " is casting " + c.toString());
      uiShowMsg("Respond with a spell/effect or press 'd' to resolve the stack.");
    }

  }

  private void handleAttack(String inputLine) {
    // setup the attack phase

    // Format is ATTACK <attacking player id> <defending player id> <iids...>

    String[] parts = inputLine.split(" ");

    int a_pid = Integer.parseInt(parts[1]);
    int d_pid = Integer.parseInt(parts[2]);

    Player ap = state.getPlayer(a_pid);
    Player dp = state.getPlayer(d_pid);

    for (int i = 3; i < parts.length; i++) {
      int iid = Integer.parseInt(parts[i]);
      Card c = ap.getCardInCreatures(iid);

      c.setAttacking(true);
      c.tap();
    }

  }

  private void handleAttackResults(String inputLine) {
    String[] parts = inputLine.split(" ");

    String lpinfo = parts[1];

    String[] keyvalpairs = lpinfo.split(",");

    for (String kvp : keyvalpairs) {
      String[] kv = kvp.split("=");
      int pid = Integer.parseInt(kv[0]);
      int lp = Integer.parseInt(kv[1]);

      Player p = state.getPlayer(pid);
      p.setLifePoints(lp);

      String prefix = (pid == me.getId() ? "You are" : (p.getName() + " is"));
      
      uiShowMsg(prefix + " now at " + lp);
    }
  }

  private void handlePlayerDraw(String inputLine) {
    String[] parts = inputLine.split(" ");

    int pid = Integer.parseInt(parts[1]);
    int num = Integer.parseInt(parts[2]);
    int libsize = Integer.parseInt(parts[3]);

    Player p = state.getPlayer(pid);

    p.setHandSize(p.getHandSize() + 1);
    p.libsize = libsize;

    String prefix = p.getName() + " draws";
    String suffix = (num == 1 ? "a card" : (num + " cards"));
    
    uiShowMsg(prefix + " " + suffix);
  }

  private void handleUntap(String inputLine) {
    // inputLine contains a list of space-separated iids that need untapping
    // Each list of iids (one per type) ends with a !
    // Lands, creatures, artifacts in that order
    String[] args = inputLine.split(" ");
    int pId = Integer.parseInt(args[1]);
    Player currentPlayer = state.getPlayer(pId);

    int i = 2;
    for (; i < args.length; i++) {
      if (args[i].equals("!")) {
        i++;
        break;
      }
      int iid = Integer.parseInt(args[i]);
      Card c = currentPlayer.getLand(iid);
      c.untap();
    }
    for (; i < args.length; i++) {
      if (args[i].equals("!")) {
        i++;
        break;
      }
      int iid = Integer.parseInt(args[i]);
      Card c = currentPlayer.getCreature(iid);
      c.untap();
    }
    for (; i < args.length; i++) {
      if (args[i].equals("!")) {
        i++;
        break;
      }
      int iid = Integer.parseInt(args[i]);
      Card c = currentPlayer.getArtifact(iid);
      c.untap();
    }
  }

  private void handleUnsick(String inputLine) {
    String[] args = inputLine.split(" ");
    int pId = Integer.parseInt(args[1]);
    Player currentPlayer = state.getPlayer(pId);

    // Remove summ. sickness from all cards
    for (Card c : currentPlayer.getLands())
      c.setSickness(false);
    for (Card c : currentPlayer.getCreatures())
      c.setSickness(false);
    for (Card c : currentPlayer.getArtifacts())
      c.setSickness(false);
    for (Card c : currentPlayer.getEnchantments())
      c.setSickness(false);
  }

  private void handleResEff(String inputLine) {
    String[] parts = inputLine.split(" ");
    CardLoader cl = new CardLoader();

    int type = Integer.parseInt(parts[1]);

    switch (type) {
    case Constants.EFTYPE_SUMMON: {
      int pid = Integer.parseInt(parts[2]);
      Player p = state.getPlayer(pid);

      Card c = cl.decode(parts[3]);

      if (c.isCreature())
        p.creatures.add(c);
      // @@@ handle cards that do not have summ. sickness
      if (c.getPermanent())
        c.setSickness(true);

      String name = (pid == me.getId() ? "You" : p.getName());
      uiShowMsg(name + " summoned " + c.toString());
    }

    }
  }

  public void handleDiscard(String inputLine) {
    String[] args = inputLine.split(" ");
    // In order, DISCARD <pid> <cid>,<iid> ... <cid>,<iid>

    int pid = Integer.parseInt(args[1]);
    Player player = state.getPlayer(pid);

    String name = (pid == me.getId() ? "You" : player.getName());
    CardLoader cl = new CardLoader();

    for (int i = 2; i < args.length; i++) {
      Card proto = cl.decode(args[i]);

      if (pid == me.getId()) {
        Card c = me.getCardInHand(proto.getIID());
        me.discard(c, null);
      } else {
        player.setHandSize(player.getHandSize() - 1);
        player.addToGraveyard(proto, null);
      }

      uiShowMsg(name + " discarded " + proto.toString());
    }
  }

  public void handleDamage(String inputLine) {
    String[] args = inputLine.split(" ");
    // The format is: DAMAGE <to> <damage> <amount>
    int pId = Integer.parseInt(args[1]);
    int toIdx = Integer.parseInt(args[2]);
    int dam = Integer.parseInt(args[3]);

    if (toIdx == -1) { // Damage to player
      Player p = state.getPlayer(pId);
      p.addLifePoints(-dam);
    } else {
      Card c = me.getCardInCreatures(toIdx);
      if (c != null)
        c.addDamage(dam);
    }
  }

  public void handleRemove(String inputLine) {
    String[] parts = inputLine.split(" ");

    // format: REMOVE <pid> <card iid>

    int pid = Integer.parseInt(parts[1]);
    int iid = Integer.parseInt(parts[2]);

    // right now only works for creatures

    Player p = state.getPlayer(pid);

    Card c = p.getCardInCreatures(iid);

    if (c != null)
      p.removeFromCreatures(c, null);
  }

  public void handleGraveyard(String inputLine) {
    String[] parts = inputLine.split(" ");

    // format: GRAVEYARD <pid> <card iid>

    int pid = Integer.parseInt(parts[1]);
    int iid = Integer.parseInt(parts[2]);

    Player p = state.getPlayer(pid);

    Card c = p.getCardInRemoved(iid);

    if (c != null)
      p.addToGraveyard(c, null);

  }

  public void handlePhase(String inputLine) {
    // Add something here?

    String[] args = inputLine.split(" ");

    if (args[1].equals(PhaseInfo.Phase.POSTMAIN.toString())) {
      state.setNotAttacking();
    }
  }
}
